// ======================================================================
// \title  FileDownlinkTester.cpp
// \author bocchino
// \brief  cpp file for FileDownlink test harness implementation class
//
// \copyright
// Copyright 2009-2015, by the California Institute of Technology.
// ALL RIGHTS RESERVED.  United States Government Sponsorship
// acknowledged.
// ======================================================================

#include <unistd.h>
#include <cerrno>

#include "FileDownlinkTester.hpp"

#define INSTANCE 0
#define CMD_SEQ 0
#define QUEUE_DEPTH 10

#define TIMEOUT_MS 1000
#define COOLDOWN_MS 500
#define CYCLE_MS 100
#define MAX_ALLOCATED 100
namespace Svc {

// ----------------------------------------------------------------------
// Construction and destruction
// ----------------------------------------------------------------------

FileDownlinkTester ::FileDownlinkTester()
    : FileDownlinkGTestBase("Tester", MAX_HISTORY_SIZE), component("FileDownlink"), buffers_index(0) {
    this->component.configure(TIMEOUT_MS, COOLDOWN_MS, CYCLE_MS, 10);
    this->connectPorts();
    this->initComponents();
}

FileDownlinkTester ::~FileDownlinkTester() {
    for (U32 i = 0; i < buffers_index; i++) {
        delete[] buffers[i];
    }
    this->component.deinit();
}

// ----------------------------------------------------------------------
// Tests
// ----------------------------------------------------------------------

void FileDownlinkTester ::downlink() {
    // Assert idle mode
    ASSERT_EQ(FileDownlink::Mode::IDLE, this->component.m_mode.get());

    // Create a file
    const char* const sourceFileName = "source.bin";
    const char* const destFileName = "dest.bin";
    U8 data[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    FileBuffer fileBufferOut(data, sizeof(data));
    fileBufferOut.write(sourceFileName);

    // Send the file and assert COMMAND_OK
    this->sendFile(sourceFileName, destFileName, Fw::CmdResponse::OK);

    // Assert telemetry
    ASSERT_TLM_SIZE(4);
    ASSERT_TLM_PacketsSent_SIZE(3);
    for (size_t i = 0; i < 3; ++i) {
        ASSERT_TLM_PacketsSent(i, i + 1);
    }
    ASSERT_TLM_FilesSent_SIZE(1);
    ASSERT_TLM_FilesSent(0, 1);

    // Assert events
    ASSERT_EVENTS_SIZE(2);
    ASSERT_EVENTS_FileSent_SIZE(1);
    ASSERT_EVENTS_SendStarted_SIZE(1);
    //    printTextLogHistory(stdout);
    ASSERT_EVENTS_SendStarted(0, 10, sourceFileName, destFileName);
    ASSERT_EVENTS_FileSent(0, sourceFileName, destFileName);

    // Validate the packet history
    History<Fw::FilePacket::DataPacket> dataPackets(MAX_HISTORY_SIZE);
    CFDP::Checksum checksum;
    fileBufferOut.getChecksum(checksum);
    validatePacketHistory(*this->fromPortHistory_bufferSendOut, dataPackets, Fw::FilePacket::T_END, 3, checksum, 0);

    // Compare the outgoing and incoming files
    FileBuffer fileBufferIn(dataPackets);
    ASSERT_EQ(true, FileBuffer::compare(fileBufferIn, fileBufferOut));

    // Assert idle mode
    ASSERT_EQ(FileDownlink::Mode::IDLE, this->component.m_mode.get());

    // Remove the outgoing file
    this->removeFile(sourceFileName);
}

void FileDownlinkTester ::fileOpenError() {
    const char* const sourceFileName = "missing_directory/source.bin";
    const char* const destFileName = "dest.bin";

    // Send the file and assert CmdResponse::EXECUTION_ERROR
    this->sendFile(sourceFileName, destFileName,
                   (FILEDOWNLINK_COMMAND_FAILURES_DISABLED) ? Fw::CmdResponse::OK : Fw::CmdResponse::EXECUTION_ERROR);

    // Assert telemetry
    ASSERT_TLM_SIZE(1);
    ASSERT_TLM_Warnings(0, 1);

    // Assert events
    ASSERT_EVENTS_SIZE(1);
    ASSERT_EVENTS_FileOpenError_SIZE(1);
    //    ASSERT_EVENTS_FileDownlink_FileOpenError(0, sourceFileName);
}

void FileDownlinkTester ::cancelDownlink() {
    // Create a file
    const char* const sourceFileName = "source.bin";
    const char* const destFileName = "dest.bin";
    U8 data[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    FileBuffer fileBufferOut(data, sizeof(data));
    fileBufferOut.write(sourceFileName);

    // Send a file
    Fw::CmdStringArg sourceCmdStringArg(sourceFileName);
    Fw::CmdStringArg destCmdStringArg(destFileName);
    this->sendCmd_SendFile(INSTANCE, CMD_SEQ, sourceCmdStringArg, destCmdStringArg);
    this->sendCmd_Cancel(INSTANCE, CMD_SEQ);
    this->component.doDispatch();       // Dispatch start command
    this->component.Run_handler(0, 0);  // Enqueue and start processing file
    this->component.doDispatch();       // Dispatch cancel command
    // Assert cancelation response
    ASSERT_CMD_RESPONSE_SIZE(1);
    ASSERT_CMD_RESPONSE(0, FileDownlink::OPCODE_CANCEL, CMD_SEQ, Fw::CmdResponse::OK);
    this->cmdResponseHistory->clear();
    ASSERT_EQ(FileDownlink::Mode::CANCEL, this->component.m_mode.get());

    this->component.doDispatch();  // Process return of original buffer and send cancel packet
    this->component.doDispatch();  // Process return of cancel packet

    // Ensure initial send file command also receives a response.
    Fw::CmdResponse resp =
        (FILEDOWNLINK_COMMAND_FAILURES_DISABLED) ? Fw::CmdResponse::OK : Fw::CmdResponse::EXECUTION_ERROR;
    ASSERT_CMD_RESPONSE_SIZE(1);
    ASSERT_CMD_RESPONSE(0, FileDownlink::OPCODE_SENDFILE, CMD_SEQ, resp);

    // Assert telemetry
    ASSERT_TLM_SIZE(2);

    // Assert cancel event
    ASSERT_EVENTS_SIZE(2);  // Started and cancel
    ASSERT_EVENTS_DownlinkCanceled_SIZE(1);

    ASSERT_EQ(FileDownlink::Mode::COOLDOWN, this->component.m_mode.get());

    this->removeFile(sourceFileName);
}

void FileDownlinkTester ::cancelInIdleMode() {
    // Assert idle mode
    ASSERT_EQ(FileDownlink::Mode::IDLE, this->component.m_mode.get());

    // Send a cancel command
    this->cancel(Fw::CmdResponse::OK);

    this->component.Run_handler(0, 0);
    // Assert idle mode
    ASSERT_EQ(FileDownlink::Mode::IDLE, this->component.m_mode.get());
}

void FileDownlinkTester ::downlinkPartial() {
    // Assert idle mode
    ASSERT_EQ(FileDownlink::Mode::IDLE, this->component.m_mode.get());

    // Create a file
    const char* const sourceFileName = "source.bin";
    const char* const destFileName = "dest.bin";
    U8 data[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    U8 dataSubset[] = {1, 2, 3, 4};
    U32 offset = 1;
    U32 length = 4;

    FileBuffer fileBufferOut(data, sizeof(data));
    fileBufferOut.write(sourceFileName);
    FileBuffer fileBufferOutSubset(dataSubset, sizeof(dataSubset));

    // Test send partial past end of file, should return COMMAND_OK but raise a warning event.
    Fw::CmdResponse expResp =
        FILEDOWNLINK_COMMAND_FAILURES_DISABLED ? Fw::CmdResponse::OK : Fw::CmdResponse::VALIDATION_ERROR;
    this->sendFilePartial(sourceFileName, destFileName, expResp, sizeof(data), length);
    ASSERT_EVENTS_SIZE(1);
    ASSERT_EVENTS_DownlinkPartialFail(0, sourceFileName, destFileName, sizeof(data), sizeof(data));
    this->cmdResponseHistory->clear();
    this->clearEvents();

    // Send the file and assert COMMAND_OK
    this->sendFilePartial(sourceFileName, destFileName, Fw::CmdResponse::OK, offset, length);

    // Assert telemetry
    ASSERT_TLM_SIZE(4);
    ASSERT_TLM_PacketsSent_SIZE(3);
    for (size_t i = 0; i < 3; ++i) {
        ASSERT_TLM_PacketsSent(i, i + 1);
    }
    ASSERT_TLM_FilesSent_SIZE(1);
    ASSERT_TLM_FilesSent(0, 1);

    // Assert events
    ASSERT_EVENTS_SIZE(2);  // Start and sent
    ASSERT_EVENTS_SendStarted_SIZE(1);
    ASSERT_EVENTS_FileSent_SIZE(1);
    //    printTextLogHistory(stdout);
    ASSERT_EVENTS_FileSent(0, sourceFileName, destFileName);
    ASSERT_EVENTS_SendStarted(0, length, sourceFileName, destFileName);

    // Validate the packet history
    History<Fw::FilePacket::DataPacket> dataPackets(MAX_HISTORY_SIZE);

    CFDP::Checksum checksum;
    checksum.update(dataSubset, offset, length);

    validatePacketHistory(*this->fromPortHistory_bufferSendOut, dataPackets, Fw::FilePacket::T_END, 3, checksum, 1);

    // Compare the outgoing and incoming files
    FileBuffer fileBufferIn(dataPackets);
    ASSERT_EQ(true, FileBuffer::compare(fileBufferIn, fileBufferOutSubset));

    // Assert idle mode
    ASSERT_EQ(FileDownlink::Mode::IDLE, this->component.m_mode.get());

    // Remove the outgoing file
    this->removeFile(sourceFileName);
}

void FileDownlinkTester ::timeout() {
    // Assert idle mode
    ASSERT_EQ(FileDownlink::Mode::IDLE, this->component.m_mode.get());

    // Create a file
    const char* const sourceFileName = "source.bin";
    const char* const destFileName = "dest.bin";
    U8 data[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    FileBuffer fileBufferOut(data, sizeof(data));
    fileBufferOut.write(sourceFileName);

    Fw::CmdStringArg sourceCmdStringArg(sourceFileName);
    Fw::CmdStringArg destCmdStringArg(destFileName);
    this->sendCmd_SendFile(INSTANCE, CMD_SEQ, sourceCmdStringArg, destCmdStringArg);
    this->component.doDispatch();       // Dispatch sendfile command
    this->component.Run_handler(0, 0);  // Pull file from queue

    // Continue running the component without dispatching the responses
    for (U32 i = 0; i < TIMEOUT_MS / CYCLE_MS; i++) {
        this->component.Run_handler(0, 0);
        ASSERT_CMD_RESPONSE_SIZE(0);
    }

    this->component.Run_handler(0, 0);
    ASSERT_CMD_RESPONSE_SIZE(1);
    Fw::CmdResponse expResp =
        FILEDOWNLINK_COMMAND_FAILURES_DISABLED ? Fw::CmdResponse::OK : Fw::CmdResponse::EXECUTION_ERROR;
    ASSERT_CMD_RESPONSE(0, FileDownlink::OPCODE_SENDFILE, CMD_SEQ, expResp);

    // Assert telemetry
    ASSERT_TLM_SIZE(1);
    ASSERT_TLM_PacketsSent_SIZE(1);

    // Assert events
    ASSERT_EVENTS_SIZE(2);
    ASSERT_EVENTS_DownlinkTimeout_SIZE(1);
    ASSERT_EVENTS_SendStarted_SIZE(1);
    //    printTextLogHistory(stdout);
    ASSERT_EVENTS_SendStarted(0, 10, sourceFileName, destFileName);
    ASSERT_EVENTS_DownlinkTimeout(0, sourceFileName, destFileName);

    // Assert idle mode
    ASSERT_EQ(FileDownlink::Mode::COOLDOWN, this->component.m_mode.get());

    // Remove the outgoing file
    this->removeFile(sourceFileName);
}

void FileDownlinkTester ::sendFilePort() {
    // Create a file
    const char* const sourceFileName = "source.bin";
    const char* const destFileName = "dest.bin";
    U8 data[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
    FileBuffer fileBufferOut(data, sizeof(data));
    fileBufferOut.write(sourceFileName);

    Fw::String srcFileArg(sourceFileName);
    Fw::String destFileArg(destFileName);
    Svc::SendFileResponse resp = this->invoke_to_SendFile(0, srcFileArg, destFileArg, 0, 0);

    ASSERT_EQ(resp.get_status(), SendFileStatus::STATUS_OK);
    ASSERT_EQ(resp.get_context(), 0);

    this->component.Run_handler(0, 0);  // Dequeue file downlink request
    while (this->component.m_mode.get() != FileDownlink::Mode::IDLE) {
        if (this->component.m_mode.get() != FileDownlink::Mode::COOLDOWN) {
            this->component.doDispatch();
        }
        this->component.Run_handler(0, 0);
    }

    ASSERT_from_FileComplete_SIZE(1);
    ASSERT_from_FileComplete(0, Svc::SendFileResponse(SendFileStatus(SendFileStatus::STATUS_OK), 0));

    // Assert telemetry
    ASSERT_TLM_SIZE(4);
    ASSERT_TLM_PacketsSent_SIZE(3);
    for (size_t i = 0; i < 3; ++i) {
        ASSERT_TLM_PacketsSent(i, i + 1);
    }
    ASSERT_TLM_FilesSent_SIZE(1);
    ASSERT_TLM_FilesSent(0, 1);

    // Assert events
    ASSERT_EVENTS_SIZE(2);
    ASSERT_EVENTS_FileSent_SIZE(1);
    ASSERT_EVENTS_SendStarted_SIZE(1);
    ASSERT_EVENTS_SendStarted(0, 10, sourceFileName, destFileName);
    ASSERT_EVENTS_FileSent(0, sourceFileName, destFileName);

    // Validate the packet history
    History<Fw::FilePacket::DataPacket> dataPackets(MAX_HISTORY_SIZE);
    CFDP::Checksum checksum;
    fileBufferOut.getChecksum(checksum);
    validatePacketHistory(*this->fromPortHistory_bufferSendOut, dataPackets, Fw::FilePacket::T_END, 3, checksum, 0);

    // Compare the outgoing and incoming files
    FileBuffer fileBufferIn(dataPackets);
    ASSERT_EQ(true, FileBuffer::compare(fileBufferIn, fileBufferOut));

    // Remove the outgoing file
    this->removeFile(sourceFileName);
}

// ----------------------------------------------------------------------
// Handlers for from ports
// ----------------------------------------------------------------------

void FileDownlinkTester ::from_bufferSendOut_handler(const FwIndexType portNum, Fw::Buffer& buffer) {
    ASSERT_LT(buffers_index, FW_NUM_ARRAY_ELEMENTS(this->buffers));
    // Copy buffer before recycling
    U8* data = new U8[buffer.getSize()];
    this->buffers[buffers_index] = data;  // NOLINT(clang-analyzer-security.ArrayBound)
    buffers_index++;
    ::memcpy(data, buffer.getData(), buffer.getSize());
    Fw::Buffer buffer_new = buffer;
    buffer_new.setData(data);
    pushFromPortEntry_bufferSendOut(buffer_new);
    invoke_to_bufferReturn(0, buffer);
}

void FileDownlinkTester ::from_pingOut_handler(const FwIndexType portNum, U32 key) {
    pushFromPortEntry_pingOut(key);
}

void FileDownlinkTester ::from_FileComplete_handler(const FwIndexType portNum, /*!< The port number*/
                                                    const Svc::SendFileResponse& response) {
    pushFromPortEntry_FileComplete(response);
}

// ----------------------------------------------------------------------
// Private instance methods
// ----------------------------------------------------------------------

void FileDownlinkTester ::connectPorts() {
    // cmdIn
    this->connect_to_cmdIn(0, this->component.get_cmdIn_InputPort(0));

    // Run
    this->connect_to_Run(0, this->component.get_Run_InputPort(0));

    // bufferReturn
    this->connect_to_bufferReturn(0, this->component.get_bufferReturn_InputPort(0));

    // Sendfile
    this->connect_to_SendFile(0, this->component.get_SendFile_InputPort(0));

    // timeCaller
    this->component.set_timeCaller_OutputPort(0, this->get_from_timeCaller(0));

    // cmdRegOut
    this->component.set_cmdRegOut_OutputPort(0, this->get_from_cmdRegOut(0));

    // eventOut
    this->component.set_eventOut_OutputPort(0, this->get_from_eventOut(0));

    // bufferSendOut
    this->component.set_bufferSendOut_OutputPort(0, this->get_from_bufferSendOut(0));

    // tlmOut
    this->component.set_tlmOut_OutputPort(0, this->get_from_tlmOut(0));

    // cmdResponseOut
    this->component.set_cmdResponseOut_OutputPort(0, this->get_from_cmdResponseOut(0));

    // LogText
    this->component.set_textEventOut_OutputPort(0, this->get_from_textEventOut(0));

    // FileComplete
    this->component.set_FileComplete_OutputPort(0, this->get_from_FileComplete(0));
}

void FileDownlinkTester ::initComponents() {
    this->init();
    this->component.init(QUEUE_DEPTH, INSTANCE);
}

void FileDownlinkTester ::sendFile(const char* const sourceFileName,
                                   const char* const destFileName,
                                   const Fw::CmdResponse response) {
    Fw::CmdStringArg sourceCmdStringArg(sourceFileName);
    Fw::CmdStringArg destCmdStringArg(destFileName);
    this->sendCmd_SendFile(INSTANCE, CMD_SEQ, sourceCmdStringArg, destCmdStringArg);

    this->component.doDispatch();
    this->component.Run_handler(0, 0);

    while (this->component.m_mode.get() != FileDownlink::Mode::IDLE) {
        if (this->component.m_mode.get() != FileDownlink::Mode::COOLDOWN) {
            this->component.doDispatch();
        }
        this->component.Run_handler(0, 0);
    }

    ASSERT_CMD_RESPONSE_SIZE(1);
    ASSERT_CMD_RESPONSE(0, FileDownlink::OPCODE_SENDFILE, CMD_SEQ, response);
}

void FileDownlinkTester ::sendFilePartial(const char* const sourceFileName,  //!< The source file name
                                          const char* const destFileName,    //!< The destination file name
                                          const Fw::CmdResponse response,    //!< The expected command response
                                          U32 startIndex,                    //!< The starting index
                                          U32 length                         //!< The amount of bytes to downlink
) {
    Fw::CmdStringArg sourceCmdStringArg(sourceFileName);
    Fw::CmdStringArg destCmdStringArg(destFileName);
    this->sendCmd_SendPartial(INSTANCE, CMD_SEQ, sourceCmdStringArg, destCmdStringArg, startIndex, length);

    this->component.doDispatch();
    this->component.Run_handler(0, 0);

    while (this->component.m_mode.get() != FileDownlink::Mode::IDLE) {
        if (this->component.m_mode.get() != FileDownlink::Mode::COOLDOWN) {
            this->component.doDispatch();
        }
        this->component.Run_handler(0, 0);
    }

    ASSERT_CMD_RESPONSE_SIZE(1);
    ASSERT_CMD_RESPONSE(0, FileDownlink::OPCODE_SENDPARTIAL, CMD_SEQ, response);
}

void FileDownlinkTester ::cancel(const Fw::CmdResponse response) {
    // Command the File Downlink component to cancel a file downlink
    this->sendCmd_Cancel(INSTANCE, CMD_SEQ);
    this->component.doDispatch();  // Cancel command processes here
    // Assert command response
    ASSERT_CMD_RESPONSE_SIZE(1);
    ASSERT_CMD_RESPONSE(0, FileDownlink::OPCODE_CANCEL, CMD_SEQ, response);
}

void FileDownlinkTester ::removeFile(const char* const name) {
    const int status = ::unlink(name);
    if (status != 0) {
        // OK if file is not there
        ASSERT_EQ(ENOENT, errno);
    }
}

// ----------------------------------------------------------------------
// Private static methods
// ----------------------------------------------------------------------

void FileDownlinkTester ::validatePacketHistory(const History<FromPortEntry_bufferSendOut>& historyIn,
                                                History<Fw::FilePacket::DataPacket>& historyOut,
                                                const Fw::FilePacket::Type finalPacketType,
                                                const size_t numPackets,
                                                const CFDP::Checksum& checksum,
                                                U32 startOffset) {
    FW_ASSERT(numPackets > 0);

    const size_t size = historyIn.size();
    ASSERT_EQ(numPackets, size);

    {
        const Fw::Buffer& buffer = historyIn.at(0).fwBuffer;
        validateStartPacket(buffer);
    }

    for (size_t i = 1; i < size - 1; ++i) {
        const Fw::Buffer& buffer = historyIn.at(i).fwBuffer;
        Fw::FilePacket::DataPacket dataPacket;
        validateDataPacket(buffer, dataPacket, i, startOffset);
        historyOut.push_back(dataPacket);
    }

    const size_t index = size - 1;
    const Fw::Buffer& buffer = historyIn.at(index).fwBuffer;
    switch (finalPacketType) {
        case Fw::FilePacket::T_END: {
            validateEndPacket(buffer, index, checksum);
            break;
        }
        case Fw::FilePacket::T_CANCEL: {
            validateCancelPacket(buffer, index);
            break;
        }
        default:
            FW_ASSERT(0);
    }
}

void FileDownlinkTester ::validateFilePacket(const Fw::Buffer& buffer, Fw::FilePacket& filePacket) {
    // buffer contains the FW_PACKET_FILE descriptor - we remove it before deserializing into the filePacket
    Fw::Buffer packetDataBuffer(buffer.getData() + sizeof(FwPacketDescriptorType),
                                buffer.getSize() - sizeof(FwPacketDescriptorType));
    const Fw::SerializeStatus status = filePacket.fromBuffer(packetDataBuffer);
    ASSERT_EQ(Fw::FW_SERIALIZE_OK, status);
}

void FileDownlinkTester ::validateStartPacket(const Fw::Buffer& buffer) {
    Fw::FilePacket filePacket;
    validateFilePacket(buffer, filePacket);
    const Fw::FilePacket::Header& header = filePacket.asHeader();
    ASSERT_EQ(0U, header.m_sequenceIndex);
    ASSERT_EQ(Fw::FilePacket::T_START, header.m_type);
}

void FileDownlinkTester ::validateDataPacket(const Fw::Buffer& buffer,
                                             Fw::FilePacket::DataPacket& dataPacket,
                                             const U32 sequenceIndex,
                                             U32& byteOffset) {
    Fw::FilePacket filePacket;
    validateFilePacket(buffer, filePacket);
    const Fw::FilePacket::Header& header = filePacket.asHeader();
    ASSERT_EQ(sequenceIndex, header.m_sequenceIndex);
    ASSERT_EQ(Fw::FilePacket::T_DATA, header.m_type);
    dataPacket = filePacket.asDataPacket();
    ASSERT_EQ(byteOffset, dataPacket.m_byteOffset);
    byteOffset += dataPacket.m_dataSize;
}

void FileDownlinkTester ::validateEndPacket(const Fw::Buffer& buffer,
                                            const U32 sequenceIndex,
                                            const CFDP::Checksum& checksum) {
    Fw::FilePacket filePacket;
    validateFilePacket(buffer, filePacket);
    const Fw::FilePacket::Header& header = filePacket.asHeader();
    ASSERT_EQ(sequenceIndex, header.m_sequenceIndex);
    ASSERT_EQ(Fw::FilePacket::T_END, header.m_type);
    const Fw::FilePacket::EndPacket endPacket = filePacket.asEndPacket();
    CFDP::Checksum computedChecksum;
    endPacket.getChecksum(computedChecksum);
    ASSERT_EQ(true, checksum == computedChecksum);
}

void FileDownlinkTester ::validateCancelPacket(const Fw::Buffer& buffer, const U32 sequenceIndex) {
    Fw::FilePacket filePacket;
    validateFilePacket(buffer, filePacket);
    const Fw::FilePacket::Header& header = filePacket.asHeader();
    ASSERT_EQ(sequenceIndex, header.m_sequenceIndex);
    ASSERT_EQ(Fw::FilePacket::T_CANCEL, header.m_type);
}

}  // namespace Svc
